package com.spicesoft.o3d.test.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.bulletphysics.gwt.client.collision.broadphase.AxisSweep3;
import com.bulletphysics.gwt.client.collision.dispatch.CollisionConfiguration;
import com.bulletphysics.gwt.client.collision.dispatch.CollisionDispatcher;
import com.bulletphysics.gwt.client.collision.dispatch.CollisionObject;
import com.bulletphysics.gwt.client.collision.dispatch.DefaultCollisionConfiguration;
import com.bulletphysics.gwt.client.collision.shapes.BoxShape;
import com.bulletphysics.gwt.client.collision.shapes.CollisionShape;
import com.bulletphysics.gwt.client.collision.shapes.SphereShape;
import com.bulletphysics.gwt.client.dynamics.DiscreteDynamicsWorld;
import com.bulletphysics.gwt.client.dynamics.RigidBody;
import com.bulletphysics.gwt.client.dynamics.RigidBodyConstructionInfo;
import com.bulletphysics.gwt.client.dynamics.constraintsolver.SequentialImpulseConstraintSolver;
import com.bulletphysics.gwt.client.linearmath.DefaultMotionState;
import com.bulletphysics.gwt.client.linearmath.Transform;
import com.bulletphysics.gwt.client.vecmath.Vector3f;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;

public class JBHelloWorldEP implements EntryPoint {
	
	private HTML debugHTML;
	
	Map<Integer, JavaScriptObject> objectsMap = new HashMap<Integer, JavaScriptObject>();

	@Override
	public void onModuleLoad() {
		
//		final String clientDivId = System.currentTimeMillis() + "_" + (int)(Math.random() * 100000);
		final String clientDivId = "o3d";
		
		SimplePanel sp = new SimplePanel();
		sp.setWidth("640px");
		sp.setHeight("480px");
		DOM.setElementAttribute(sp.getElement(), "id", clientDivId);
		
		VerticalPanel vp = new VerticalPanel();
		
		vp.add(sp);
		debugHTML = new HTML();
		debugHTML.setHTML("Click to spawn new objects.");
		DOM.setElementAttribute(debugHTML.getElement(), "id", "debug");
		vp.add(debugHTML);
		
		RootPanel.get("main").add(vp);
		
		final JBHelloWorldEP thisObj = this;
		
		DeferredCommand.addCommand(new Command() {
			@Override
			public void execute() {
				init(thisObj, clientDivId, false);
			}
		});
	}

	public native void init(JBHelloWorldEP x, String id, boolean useV8)/*-{
		if (useV8) {
			$wnd.o3djs.util.setMainEngine($wnd.o3djs.util.Engine.V8);
		}
		$wnd.o3djs.util.makeClients(
			function (clientElements) {
				var o3dElement = clientElements[0];
				var g_client = o3dElement.client;
				$wnd.cycle = 0;
				x.@com.spicesoft.o3d.test.client.JBHelloWorldEP::initCallback(Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(o3dElement, g_client);
				g_client.setRenderCallback(function(re) {
					x.@com.spicesoft.o3d.test.client.JBHelloWorldEP::renderCallback(Lcom/google/gwt/core/client/JavaScriptObject;D)(re, re.elapsedTime);
				});
			},
			null,
			null,
			id
		);
	}-*/;
	
	private void initCallback(JavaScriptObject clientElement, JavaScriptObject client) {
		lala(this, clientElement, client);
		initPhysics();
	}

	private void initPhysics() {
		// collision configuration contains default setup for memory, collision
		// setup. Advanced users can create their own configuration.
		CollisionConfiguration collisionConfiguration = new DefaultCollisionConfiguration();

		// use the default collision dispatcher. For parallel processing you
		// can use a diffent dispatcher (see Extras/BulletMultiThreaded)
		CollisionDispatcher dispatcher = new CollisionDispatcher(
				collisionConfiguration);
		
		// the maximum size of the collision world. Make sure objects stay
		// within these boundaries
		// Don't make the world AABB size too large, it will harm simulation
		// quality and performance
		Vector3f worldAabbMin = new Vector3f(-100, -100, -100);
		Vector3f worldAabbMax = new Vector3f(100, 100, 100);
		int maxProxies = 1024;
		AxisSweep3 overlappingPairCache =
				new AxisSweep3(worldAabbMin, worldAabbMax, maxProxies);
		//BroadphaseInterface overlappingPairCache = new SimpleBroadphase(
		//		maxProxies);

		// the default constraint solver. For parallel processing you can use a
		// different solver (see Extras/BulletMultiThreaded)
		SequentialImpulseConstraintSolver solver = new SequentialImpulseConstraintSolver();

		dynamicsWorld = new DiscreteDynamicsWorld(
				dispatcher, overlappingPairCache, solver,
				collisionConfiguration);

		dynamicsWorld.setGravity(new Vector3f(0, -30, 0));

		collisionShapes = new ArrayList<CollisionShape>();
		{
			// create a few basic rigid bodies
			CollisionShape groundShape = new BoxShape(new Vector3f(10.f, 1.f, 10.f));

			collisionShapes.add(groundShape);
			Transform groundTransform = new Transform();
			groundTransform.setIdentity();
			groundTransform.origin.set(new Vector3f(0.f, -1.f, 0.f));

			float mass = 0f;

			// rigidbody is dynamic if and only if mass is non zero,
			// otherwise static
			boolean isDynamic = (mass != 0f);

			Vector3f localInertia = new Vector3f(0, 10, 0);
			if (isDynamic) {
				groundShape.calculateLocalInertia(mass, localInertia);
			}

			// using motionstate is recommended, it provides interpolation
			// capabilities, and only synchronizes 'active' objects
			DefaultMotionState myMotionState = new DefaultMotionState(groundTransform);
			RigidBodyConstructionInfo rbInfo = new RigidBodyConstructionInfo(
					mass, myMotionState, groundShape, localInertia);
			RigidBody body = new RigidBody(rbInfo);
			body.setFriction(0.5f);

			// add the body to the dynamics world
			dynamicsWorld.addRigidBody(body);
		}

		{
			// create a few basic rigid bodies
			CollisionShape groundShape = new BoxShape(new Vector3f(30.f, 1.f, 30.f));

			collisionShapes.add(groundShape);
			Transform groundTransform = new Transform();
			groundTransform.setIdentity();
			groundTransform.origin.set(new Vector3f(0.f, -9.f, 0.f));

			float mass = 0f;

			// rigidbody is dynamic if and only if mass is non zero,
			// otherwise static
			boolean isDynamic = (mass != 0f);

			Vector3f localInertia = new Vector3f(0, 0, 0);
			if (isDynamic) {
				groundShape.calculateLocalInertia(mass, localInertia);
			}

			// using motionstate is recommended, it provides interpolation
			// capabilities, and only synchronizes 'active' objects
			DefaultMotionState myMotionState = new DefaultMotionState(groundTransform);
			RigidBodyConstructionInfo rbInfo = new RigidBodyConstructionInfo(
					mass, myMotionState, groundShape, localInertia);
			RigidBody body = new RigidBody(rbInfo);
			body.setFriction(0.5f);

			// add the body to the dynamics world
			dynamicsWorld.addRigidBody(body);
		}
		
		float boxWeight = 0.5f;

		/*
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 0.5f, 0.5f})), 1f, new Vector3f(new float[]{-4f, 11f, 4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 0.5f, 0.5f})), 1f, new Vector3f(new float[]{4f, 11f, -4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 0.5f, 0.5f})), 1f, new Vector3f(new float[]{-4f, 11f, -4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 0.5f, 0.5f})), 1f, new Vector3f(new float[]{4f, 11f, 4f}), null);
		*/
		
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{-4f, 5f, -4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{0f, 5f, -4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{4f, 5f, -4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{4f, 5f, 0f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{-4f, 5f, 4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{0f, 5f, 4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{-4f, 5f, 0f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{4f, 5f, 4f}), null);
		addObject(new BoxShape(new Vector3f(new float[]{0.5f, 5f, 0.5f})), boxWeight, new Vector3f(new float[]{0f, 5f, 0f}), null);

		addObject(new BoxShape(new Vector3f(new float[]{8f, 0.5f, 8f})), 1, new Vector3f(new float[]{0f, 10f, 0f}), null);

//		for (float i = -2; i <= 2; i += 2) {
//			for (float j = 0; j <= 4; j += 2) {
//				for (float k = -2; k <= 2; k += 2) {
//					addObject(new BoxShape(new Vector3f(new float[]{1f, 1f ,1f})), boxWeight, new Vector3f(new float[]{i, j, k}), null);
//				}
//			}
//		}
	}
	
	private void addObject(CollisionShape colShape, float mass, Vector3f origin, Vector3f initialLiniarVelocity) {
		collisionShapes.add(colShape);

		Transform startTransform = new Transform();
		startTransform.setIdentity();
		
		boolean isDynamic = (mass != 0f);
		Vector3f localInertia = new Vector3f(0, 10, 0);
		if (isDynamic) {
			colShape.calculateLocalInertia(mass, localInertia);
		}
		
		startTransform.origin.set(origin);
		
		DefaultMotionState myMotionState = new DefaultMotionState(startTransform);
		
		RigidBodyConstructionInfo rbInfo = new RigidBodyConstructionInfo(
				mass, myMotionState, colShape, localInertia);
		RigidBody body = new RigidBody(rbInfo);
		body.setFriction(0.5f);
		if (initialLiniarVelocity != null) {
			body.setLinearVelocity(initialLiniarVelocity);
		}
		dynamicsWorld.addRigidBody(body);
		
		JavaScriptObject transform = null;
		if (colShape instanceof SphereShape) {
			transform = drawShere(((SphereShape) colShape).getRadius());
		} else if (colShape instanceof BoxShape) {
			Vector3f dimentions = new Vector3f();
			((BoxShape) colShape).getHalfExtentsWithMargin(dimentions);
			transform = drawBox(dimentions.x * 2, dimentions.y * 2, dimentions.z * 2);
		} 
		
		objectsMap.put(body.debugBodyId, transform);
	}

	private native JavaScriptObject drawBox(float x, float y, float z)/*-{
		var shape = $wnd.o3djs.primitives.createBox($wnd.g_pack, $wnd.redMaterial, x, y, z);

	    var transform = $wnd.g_pack.createObject('Transform');
    	transform.addShape(shape);
    	transform.translate([0, 0, 0]);
    	transform.parent = $wnd.o3dClient.root;
    	
    	return transform;
	}-*/;

	private native JavaScriptObject drawShere(float radius)/*-{
		var shape = $wnd.o3djs.primitives.createSphere($wnd.g_pack, $wnd.redMaterial, radius, 30, 10);

	    var transform = $wnd.g_pack.createObject('Transform');
    	transform.addShape(shape);
    	transform.translate([0, 0, 0]);
    	transform.parent = $wnd.o3dClient.root;
    	
    	return transform;
	}-*/;
	
	
	private DiscreteDynamicsWorld dynamicsWorld;
	
	private double cycle = 0;

	private List<CollisionShape> collisionShapes;
	
	private void renderCallback(JavaScriptObject renderCallback, double elapsedTime) {
		
		cycle = (cycle + elapsedTime / 5) % 360;
		
		dynamicsWorld.stepSimulation((float) elapsedTime, 1);

		for (int j = 0; j < dynamicsWorld.getNumCollisionObjects(); j++) {
			CollisionObject obj = dynamicsWorld.getCollisionObjectArray().get(j);
			RigidBody body = RigidBody.upcast(obj);
			if (body != null && body.getMotionState() != null) {
				Transform trans = new Transform();
				body.getMotionState().getWorldTransform(trans);
				float[] localmatrix = new float[16];
				trans.getOpenGLMatrix(localmatrix);
				
				if (objectsMap.get(body.debugBodyId) != null) { 
					setTransformLocalMatrix(objectsMap.get(body.debugBodyId), localmatrix[0], localmatrix[1], localmatrix[2], localmatrix[3], localmatrix[4], localmatrix[5], localmatrix[6], localmatrix[7], localmatrix[8], localmatrix[9], localmatrix[10], localmatrix[11], localmatrix[12], localmatrix[13], localmatrix[14], localmatrix[15]);
				}
			}
		}
		
		setCameraEye(10f + 70f * Math.sin(cycle), 30d + 20f * Math.cos(cycle * 3), 20f + 70f * Math.cos(cycle));
	}
	
	private native void dbg(String s) /*-{
		$wnd.document.getElementById("debug").innerHTML = s;
	}-*/;
	
	private native void setTransformLocalMatrix(JavaScriptObject transform, float a0, float a1, float a2, float a3, float a4, float a5, float a6, float a7, float a8, float a9, float a10, float a11, float a12, float a13, float a14, float a15) /*-{
		transform.localMatrix = [
		    [a0, a1, a2, a3],
		    [a4, a5, a6, a7],
		    [a8, a9, a10, a11],
		    [a12, a13, a14, a15]
	  	];
	}-*/;
	
	private void spawnNewObject() {
		Vector3f origin = new Vector3f(new float[]{7f -  (float)(14f * Math.random()), 30f + (float)(20f * Math.random()), 7f - (float)(14f * Math.random())});
		if (((int)(10 * Math.random())) % 2 == 0) {
			addObject(new SphereShape(1.5f), 2f, origin, null);
		} else {
			addObject(new BoxShape(new Vector3f(new float[]{1f, 1f, 1f})), 2f, origin, null);
		}
	}
 	
	private native void lala(JBHelloWorldEP x, JavaScriptObject clientElement, JavaScriptObject client)/*-{
		var g_pack = client.createPack();
		$wnd.g_pack = g_pack;
		
		var g_math = $wnd.o3djs.math;
		$wnd.g_math = g_math;
		
		$wnd.o3dClient = client;

  		var viewInfo = $wnd.o3djs.rendergraph.createBasicView(
      		g_pack,
      		client.root,
      		client.renderGraphRoot);
      		
      	$wnd.o3djs.event.addEventListener(clientElement, 'mousedown', function() {
      		x.@com.spicesoft.o3d.test.client.JBHelloWorldEP::spawnNewObject()();
      	});

		$wnd.viewInfo = viewInfo;

		// Create a new, empty Material object.
		var redMaterial = g_pack.createObject('Material');

		var g_lightPosition = [0, 40, 0];
		
		$wnd.o3djs.effect.attachStandardShader(
		    g_pack, redMaterial, g_lightPosition, 'phong');
		
		redMaterial.drawList = viewInfo.performanceDrawList;
		
		// Assign parameters to the phong material.
		redMaterial.getParam('emissive').value = [0, 0, 0, 1];
		redMaterial.getParam('ambient').value = g_math.mul(0.1, [1,0,0,1]);
		redMaterial.getParam('diffuse').value = g_math.mul(0.9, [1,0,0,1]);
		redMaterial.getParam('specular').value = [.2, .2, .2, 1];
		redMaterial.getParam('shininess').value = 20;

		$wnd.redMaterial = redMaterial;

		// Create a new, empty Material object.
		var blueMaterial = g_pack.createObject('Material');	

		var g_lightPosition = [5, 5, 7];
		
		$wnd.o3djs.effect.attachStandardShader(
		    g_pack, blueMaterial, g_lightPosition, 'phong');
		
		blueMaterial.drawList = viewInfo.performanceDrawList;
		
		// Assign parameters to the phong material.
		blueMaterial.getParam('emissive').value = [0, 0, 0, 1];
		blueMaterial.getParam('ambient').value = g_math.mul(0.1, [0,1,0,1]);
		blueMaterial.getParam('diffuse').value = g_math.mul(0.9, [0,1,0,1]);
		blueMaterial.getParam('specular').value = [.2, .2, .2, 1];
		blueMaterial.getParam('shininess').value = 20;

		$wnd.blueMaterial = blueMaterial;

		var ground1 = $wnd.o3djs.primitives.createBox(g_pack, blueMaterial, 20, 2, 20);
		var ground2 = $wnd.o3djs.primitives.createBox(g_pack, blueMaterial, 60, 2, 60);

	    var groundTransform1 = g_pack.createObject('Transform');
    	groundTransform1.addShape(ground1);
    	groundTransform1.translate([0, -1, 0]);
    	groundTransform1.parent = client.root;
		
	    var groundTransform2 = g_pack.createObject('Transform');
    	groundTransform2.addShape(ground2);
    	groundTransform2.translate([0, -9, 0]);
    	groundTransform2.parent = client.root;
		
		viewInfo.drawContext.projection = g_math.matrix4.perspective(
      		g_math.degToRad(30), // 30 degree frustum.
      		clientElement.clientWidth / clientElement.clientHeight, // Aspect ratio.
      		1,                  // Near plane.
      		5000);              // Far plane.

  		var eye = [100, 30, 100];
  		var target = [0, 0, 0];
  		var up = [0, 1, 0];
  
  		viewInfo.drawContext.view = g_math.matrix4.lookAt(eye, target, up);
	}-*/;
	
	
	
	private native void setCameraEye(double x, double y, double z)/*-{
  		var eye = [x, y, z];
  		var target = [0, 0, 0];
  		var up = [0, 1, 0];
  
  		$wnd.viewInfo.drawContext.view = $wnd.g_math.matrix4.lookAt(eye, target, up);
	}-*/;

}
